package com.cmc6.minio.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.text.StrBuilder;
import cn.hutool.core.thread.ThreadUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.core.util.URLUtil;
import com.cmc6.common.exception.BaseBizCodeEnum;
import com.cmc6.common.util.ApiResult;
import com.cmc6.common.util.MyEntityUtil;
import com.cmc6.common.util.MyFileTypeUtil;
import com.cmc6.common.util.UserUtil;
import com.cmc6.minio.model.dto.FileDownloadDTO;
import com.cmc6.minio.model.dto.FileRemoveDTO;
import com.cmc6.minio.model.entity.EsFileDeleteErrorDO;
import com.cmc6.minio.model.entity.FileDO;
import com.cmc6.minio.repository.EsFileDeleteErrorRepository;
import com.cmc6.minio.service.FileDbService;
import com.cmc6.minio.service.FileService;
import io.minio.*;
import io.minio.messages.DeleteError;
import io.minio.messages.DeleteObject;
import lombok.SneakyThrows;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;

import javax.annotation.Resource;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import java.io.InputStream;
import java.util.*;

@Service
public class FileServiceImpl implements FileService {

    @Resource
    MinioClient minioClient;
    @Resource
    FileDbService fileDbService;
    @Resource
    HttpServletResponse response;
    @Resource
    EsFileDeleteErrorRepository esFileDeleteErrorRepository;

    /**
     * 文件上传
     */
    @SneakyThrows
    @Override
    @Transactional
    public String upload(MultipartFile file, String bucketName, String folderName, String remark, String extraJson) {

        String originalFilename = file.getOriginalFilename();

        if (originalFilename == null) {
            ApiResult.error("操作失败：文件名不能为空");
        }

        Long userId = UserUtil.getCurrentUserId(); // 当前 userId

        createBucket(bucketName); // 不存在则创建桶

        // 把文件夹路径合法化
        List<String> split = StrUtil.splitTrim(folderName, '/');
        if (split.size() == 0) {
            ApiResult.error("操作失败：文件夹名不合法");
        }

        // 拼接路径
        String path = userId + "/" + CollUtil.join(split, "/") + "/";

        InputStream inputStream = file.getInputStream();
        String fileType = MyFileTypeUtil.getType(file);

        // 检验文件类型
        if (fileType == null) {
            ApiResult.error("操作失败：暂不支持此文件类型【" + originalFilename + "】");
        }

        // 新的文件名
        String newFileName = IdUtil.simpleUUID() + originalFilename.substring(originalFilename.lastIndexOf("."));

        StrBuilder strBuilder = StrBuilder.create();

        String fileFullPath = path + newFileName;

        // 上传
        upload(bucketName, fileFullPath, fileType, inputStream);

        String url = strBuilder.append("/").append(bucketName).append("/").append(fileFullPath).toString();

        FileDO entity = new FileDO();
        entity.setUrl(url);
        entity.setFileName(originalFilename); // 存储文件名
        entity.setExtraJson(MyEntityUtil.getNotNullStr(extraJson)); // 额外信息（json格式）

        // 存储本次上传记录，文件url就是主键id
        fileDbService.save(entity);

        return url;
    }

    /**
     * 创建桶
     */
    @SneakyThrows
    public void createBucket(String bucketName) {
        if (!bucketExists(bucketName)) {
            minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
        }
    }

    /**
     * 备注：文件名相同会被覆盖掉
     */
    @SneakyThrows
    public void upload(String bucketName, String fileName, String fileType, InputStream inputStream) {
        minioClient.putObject(PutObjectArgs.builder().bucket(bucketName).contentType(fileType).object(fileName)
            .stream(inputStream, -1, ObjectWriteArgs.MAX_PART_SIZE).build());
    }

    /**
     * 以流的形式获取一个文件对象（断点下载）
     */
    @SneakyThrows
    public InputStream getObject(String bucketName, String objectName, long offset, Long length) {
        boolean flag = bucketExists(bucketName);
        if (flag) {
            StatObjectResponse statObject = statObject(bucketName, objectName);
            if (statObject != null && statObject.size() > 0) {
                return minioClient.getObject(
                    GetObjectArgs.builder().bucket(bucketName).object(objectName).offset(offset).length(length)
                        .build());
            }
        }
        return null;
    }

    /**
     * 以流的形式获取一个文件对象
     */
    @SneakyThrows
    public InputStream getObject(String bucketName, String objectName) {
        boolean flag = bucketExists(bucketName);
        if (flag) {
            StatObjectResponse statObject = statObject(bucketName, objectName);
            if (statObject != null && statObject.size() > 0) {
                return minioClient.getObject(GetObjectArgs.builder().bucket(bucketName).object(objectName).build());
            }
        }
        return null;
    }

    /**
     * 获取对象的元数据，用于判断对象是否存在
     */
    public StatObjectResponse statObject(String bucketName, String objectName) {
        try {
            return minioClient.statObject(StatObjectArgs.builder().bucket(bucketName).object(objectName).build());
        } catch (Exception e) {
            return null;
        }
    }

    /**
     * 判断桶是否存在
     */
    @SneakyThrows
    private boolean bucketExists(String bucketName) {
        if (StrUtil.isBlank(bucketName)) {
            ApiResult.error("判断文件桶是否存在失败：bucketName 为空");
        }
        return minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
    }

    /**
     * 公用，文件下载（不需要登录）
     */
    @Override
    public void publicDownload(String uri) {

        // 只能下载：包含 /public/ 的文件
        if (!uri.contains("/public/")) {
            ApiResult.error("操作失败：文件uri不合法：" + uri);
        }

        download(new FileDownloadDTO(uri));
    }

    /**
     * 文件下载
     */
    @SneakyThrows
    @Override
    public void download(FileDownloadDTO dto) {

        List<String> splitTrimList = StrUtil.splitTrim(dto.getUri(), "/");

        if (splitTrimList.size() < 2) {
            ApiResult.error("操作失败：url不合法：" + dto.getUri());
        }

        String bucketName = splitTrimList.get(0);

        InputStream inputStream = getObject(bucketName, dto.getUri().split("/" + bucketName)[1]);
        if (inputStream != null) {
            ServletOutputStream outputStream = response.getOutputStream();
            IoUtil.copy(inputStream, outputStream);
            outputStream.flush();
            IoUtil.close(inputStream);
            IoUtil.close(outputStream);
        } else {
            ApiResult.error("操作失败：文件不存在");
        }

    }

    /**
     * 文件删除
     */
    @SneakyThrows
    @Override
    @Transactional
    public String remove(FileRemoveDTO dto) {
        Long userId = UserUtil.getCurrentUserId();
        HashMap<String, Set<String>> delMap = MapUtil.newHashMap();
        for (String item : dto.getUrlSet()) {
            String str = URLUtil.getPath(item); // 得到 /bucketName/userId/folderName/fileName.xxx
            List<String> splitTrimList = StrUtil.splitTrim(str, "/");
            if (splitTrimList.size() < 4) {
                ApiResult.error("操作失败：url不合法：" + item);
            }
            // 只能删除自己的文件
            if (!(userId + "").equals(splitTrimList.get(1))) {
                ApiResult.error("操作失败：只能删除自己的文件：" + item);
            }
            String bucketName = splitTrimList.get(0);
            Set<String> orDefaultSet = delMap.getOrDefault(bucketName, new HashSet<>());
            orDefaultSet.add(str.split("/" + bucketName)[1]); // 添加：/userId/folderName/fileName.xxx
            delMap.put(bucketName, orDefaultSet);
        }

        List<EsFileDeleteErrorDO> esFileDeleteErrorInsertList = new ArrayList<>(); // 保存错误信息到 es里

        for (Map.Entry<String, Set<String>> item : delMap.entrySet()) {
            // 删除文件服务器的内容
            Iterable<Result<DeleteError>> resultIterable = removeObject(item.getKey(), item.getValue());

            if (resultIterable != null) {
                for (Result<DeleteError> subItem : resultIterable) {
                    esFileDeleteErrorInsertList.add(BeanUtil.copyProperties(subItem.get(), EsFileDeleteErrorDO.class));
                }
            }
        }

        // 存储删除失败的信息到 es
        if (esFileDeleteErrorInsertList.size() != 0) {
            ThreadUtil.execute(() -> esFileDeleteErrorRepository.saveAll(esFileDeleteErrorInsertList));
        }

        // 删除数据库的内容
        fileDbService.lambdaUpdate().in(FileDO::getUrl, dto.getUrlSet()).remove();

        return BaseBizCodeEnum.API_RESULT_OK.getMsg();
    }

    /**
     * 删除指定桶的多个文件对象，返回删除错误的对象列表，全部删除成功，则返回空列表
     */
    @SneakyThrows
    public Iterable<Result<DeleteError>> removeObject(String bucketName, Set<String> objectNameSet) {
        boolean flag = bucketExists(bucketName);

        if (flag) {

            List<DeleteObject> objectList = new ArrayList<>();
            for (String item : objectNameSet) {
                objectList.add(new DeleteObject(item));
            }

            return minioClient
                .removeObjects(RemoveObjectsArgs.builder().bucket(bucketName).objects(objectList).build());
        }
        return null;
    }
}
